package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.client.RedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;


//返回形如：[2,25,225],这样可以转化成2/25/225  最后前端可以转化成  手机/手机通讯/xxxx  的完整分类路径
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    //缓存的入门：把Map当作缓存
    Map<String, Object> cache = new HashMap<>();

    //缓存初步：把“一个”redis当作缓存
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //缓存进阶：使用redisson作为分布式锁
    @Resource
    private RedissonClient redissonClient;

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Override
    public Long[] findCatelodPath(Long catelogId) {
        List<Long> path = new ArrayList<>();
        //我们需要递归的查找父id，因此抽取为一个方法
        List<Long> parentPath = this.findParentPath(catelogId, path);
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[parentPath.size()]);
    }

    /**
     * 级联更新所有关联的数据
     * @CacheEvict : 失效模式的使用
     *
     *  @Caching: 同时进行多种缓存操作的组合使用
     *
     *  @CacheEvict(value = "category",allEntries = true): 指定删除某个分区下的所有数据
     * @param category
     */
    @Override

//    @Caching(evict = {
//            @CacheEvict(value = {"category"},key = "'getLevelFirstCategories'"),
//            @CacheEvict(value = {"category"},key = "'getCatalogJson'")
//    })
    @CacheEvict(value = "category",allEntries = true)  //指定删除某个分区下的所有数据
    @Transactional
    public void updateCascade(CategoryEntity category) {
        baseMapper.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

    @Override
    @Cacheable(value = {"category"},key = "#root.method.name")   //代表当前方法的结果需要缓存,给“分类业务”区缓存存放数据
    public List<CategoryEntity> getLevelFirstCategories() {
        System.out.println("访问了getLevelFirstCategories方法。。。");
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }


    /**
     * 我们最新使用SpringCache做缓存
     * @return
     */
    @Cacheable(value = {"category"},key = "#root.method.name")
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        //查询三级分类的业务代码优化
        //1.将数据库的多次查询变为一次
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);

        //1.先查询出所有的1级分类
        List<CategoryEntity> levelFirstCategories = getParent_cid(categoryEntities, 0l);
        Map<String, List<Catelog2Vo>> parent_cid = levelFirstCategories.stream().collect(Collectors.toMap(k -> {
            return k.getCatId().toString();
        }, v -> {
            List<Catelog2Vo> collect = null;
            //找当前一级分类的所有二级分类构成的List<Catelog2Vo>
            List<CategoryEntity> categoryEntitiesLevel2 = getParent_cid(categoryEntities, v.getCatId());
            if (categoryEntitiesLevel2 != null) {
                collect = categoryEntitiesLevel2.stream().map((level2) -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());

                    //1.找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> categoryEntitiesLevel3 = getParent_cid(categoryEntities, level2.getCatId());
                    List<Catelog2Vo.Catelog3Vo> middle = null;
                    if (categoryEntitiesLevel3 != null) {
                        middle = categoryEntitiesLevel3.stream().map((level3) -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                    }

                    catelog2Vo.setCatalog3List(middle);
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return collect;
        }));
        return parent_cid;
    }


    /**
     * 最早的使用redis做缓存，太麻烦了我们把它淘汰掉
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJson1() {
        //1.加入缓存逻辑
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String s = ops.get("catalogJson");
        //2.缓存中没有，查询数据库库
        if (StringUtils.isEmpty(s)) {
            Map<String, List<Catelog2Vo>> catalogJson = getCatalogJsonFromDbWithRedisLock();
            //我们应该把接下来三行代码放到getCatalogJsonFromDb()方法中，因为查数据库操作
            //要和将结果放入缓存在同一把锁中操作(原子操作)，否则可能由于放入缓存这个操作慢半拍导致重新
            //查了一次数据库

            //3.将那个查询到的数据放入缓存,先将查出的对象转换为json字符串，序列化
//            String toJSONString = JSON.toJSONString(catalogJson);
//            ops.set("catalogJson",toJSONString,1, TimeUnit.DAYS);
//
//            return catalogJson;
        }
        //将查询出的字符串转化为对象，反序列化，并返回
        TypeReference<Map<String, List<Catelog2Vo>>> typeReference = new TypeReference<Map<String, List<Catelog2Vo>>>() {
        };
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(s, typeReference);
        return result;
    }


    /**
     * 使用Redis作为分布式锁
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();

        //1.我们使用redis占用分布式锁，并同时设置过期时间，及时把锁删除，否则会出现死锁的现象
        //为了防止出现删掉别的节点的锁的情况，我们给value设为一个uuid
        String uuid = UUID.randomUUID().toString();
        Boolean lock = ops.setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if (lock) {
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            //如果加锁成功，执行下面逻辑
            try{
                dataFromDb = getDataFromDb(ops);
            }finally {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
                        , Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        }else{
            //如果自旋的频率太高了，我们可以让节点休眠一段时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonFromDbWithRedisLock(); //采用自旋的方式不断获取锁，直到获取成功
        }
    }

    /**
     * 使用redisson作为分布式锁
     * 缓存里面的数据如何和数据库里面的数据保持一致
     * 这就是缓存数据的一致性问题
     * 一般有以下两个场景：
     * 1）双写模式：改完数据库，同时更新缓存
     *      容易产生脏数据现象，解决方案：
     *      a）给双写操作加锁
     *      b）容忍数据的最终一致性，设置过期时间，在这段时间内，数据有可能是不一致的
     * 2）失效模式：改完数据库，删除缓存
     *      也可能会发生脏数据现象，解决方案：
     *      这种情况会发生在读写并发进行的场景下，因此我们可以加读写锁
     *
     *
     * 我们系统的一致性解决方案：
     * 1.缓存的所有数据都有过期时间，数据过期触发主动过期
     * 2.读写数据的时候，加上分布式读写锁。在读多写少的场景下和没加锁性能是一样的
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        Map<String, List<Catelog2Vo>> dataFromDb = null;
        //如果加锁成功，执行下面逻辑

        RLock lock = redissonClient.getLock("catalogJson-lock");
        lock.lock();
        try{
            dataFromDb = getDataFromDb(ops);
        }finally {
            lock.unlock();
        }
        return dataFromDb;
    }


    private Map<String, List<Catelog2Vo>> getDataFromDb(ValueOperations<String, String> ops) {
        //得到锁以后，我们应该再去缓存中确定一次，如果没有才需要继续查询

        String s = ops.get("catalogJson");
        if (!StringUtils.isEmpty(s)) {
            TypeReference<Map<String, List<Catelog2Vo>>> typeReference = new TypeReference<Map<String, List<Catelog2Vo>>>() {
            };
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(s, typeReference);
            return result;
        }


        //查询三级分类的业务代码优化
        //1.将数据库的多次查询变为一次
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);

        //1.先查询出所有的1级分类
        List<CategoryEntity> levelFirstCategories = getParent_cid(categoryEntities, 0l);
        Map<String, List<Catelog2Vo>> parent_cid = levelFirstCategories.stream().collect(Collectors.toMap(k -> {
            return k.getCatId().toString();
        }, v -> {
            List<Catelog2Vo> collect = null;
            //找当前一级分类的所有二级分类构成的List<Catelog2Vo>
            List<CategoryEntity> categoryEntitiesLevel2 = getParent_cid(categoryEntities, v.getCatId());
            if (categoryEntitiesLevel2 != null) {
                collect = categoryEntitiesLevel2.stream().map((level2) -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());

                    //1.找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> categoryEntitiesLevel3 = getParent_cid(categoryEntities, level2.getCatId());
                    List<Catelog2Vo.Catelog3Vo> middle = null;
                    if (categoryEntitiesLevel3 != null) {
                        middle = categoryEntitiesLevel3.stream().map((level3) -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                    }

                    catelog2Vo.setCatalog3List(middle);
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return collect;
        }));
        String toJSONString = JSON.toJSONString(parent_cid);
        ops.set("catalogJson", toJSONString, 1, TimeUnit.DAYS);

        return parent_cid;
    }


    /**
     * 真正从数据库中查询出数据，而不是从缓存中
     * 一下子查出所有的分类，并按照要求进行返回
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() {

        //使用本地缓存在分布式场景下出现的问题：
        //1.所有节点的缓存数据不一致，可能还是会重复查询
        //2.某一台节点的数据进行修改只能刷新该节点的本地缓存，对于其他节点来讲，可能会出现数据一致性问题
        //如果缓存中有就先用缓存的
//        Map<String, List<Catelog2Vo>> catalogJson = (Map<String, List<Catelog2Vo>>) cache.get("catalogJson");
//        if(catalogJson == null){
        //先调用业务
        //........
        //将数据放入缓存
//            cache.put("catalogJson",parent_cid);
//            return parent_cid;
//        }
//        return catalogJson;

        /*
        为了解决缓存击穿问题，我们在真正查询数据库的操作上加一把锁
        由于springboot中，默认所有的bean都是单例的，因此this这个对象只有一份，可以作为锁
         */
        //TODO 本地锁（synchronized/JUC）  在分布式场景下，想要锁住所有微服务实例，必须使用分布式锁
        synchronized (this){

            //得到所以后，我们应该再去缓存中确定一次，如果没有才需要继续查询
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            return getDataFromDb(ops);
        }


    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> categoryEntities,Long parent_cid) {
        List<CategoryEntity> collect = categoryEntities.stream().filter((item) -> {
            return item.getParentCid() == parent_cid;
        }).collect(Collectors.toList());
        return collect;
//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }


    private List<Long> findParentPath(Long catelogId,List<Long> path){
        //1.收集当前节点id
        path.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if(byId.getParentCid() != 0){
            findParentPath(byId.getParentCid(),path);
        }
        return path;
    }


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //2.组装成父子树形结构
        //2.1 找到所有一级分类
            /*
            所有一级分类的特点：其父id是0，而其他级别的id都是各种数字
             */
        List<CategoryEntity> level1Menus = entities.stream()
                .filter((categoryEntity) -> {
//                    if(categoryEntity.getParentCid() == null){
//                        return false;
//                    }else{
                    return categoryEntity.getParentCid() == 0;

                })
                .map((menu) -> {
                    menu.setChildren(getChildrens(menu,entities));
                    return menu;
                })
                .sorted((m,n) -> {
                    return (m.getSort()==null?0:m.getSort()) - (n.getSort()==null?0:n.getSort());
                })
                .collect(Collectors.toList());

        return level1Menus;
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO  1.检查当前删除的菜单是否被别的地方引用，如果引用了就不能删除

        //下面的这种删除是将数据库中的多条表记录真正删除了，但在实际开发当中
        //这种做法并不常见，真正的做法是“逻辑”删除，即表记录还留着，只不过修改的
        //是这些记录的状态信息，我们在gulimall-pms中的pms_category这张表中
        //有一个字段：show_status表示的就是提到的状态信息，如果是1代表"逻辑上"
        //存在，如果是0代表“逻辑上”已经被删除，并通过一些手段让数据不显示到页面中。
        //在这里我们使用的是Mybatis-Plus的逻辑删除功能
        baseMapper.deleteBatchIds(asList);
    }

    private List<CategoryEntity> getChildrens(CategoryEntity menu ,List<CategoryEntity> entities ){
        List<CategoryEntity> collect = entities.stream()
                .filter((m) -> {
//                    if(m.getParentCid() == null){
//                        return false;
//                    }else{
                    return (m.getParentCid()).equals(menu.getCatId());

                })
                .map((m) -> {
                    //找到子菜单
                    m.setChildren(getChildrens(m, entities));
                    return m;
                })
                .sorted((m, n) -> {
                    //菜单的排序
                    return (m.getSort()==null?0:m.getSort()) - (n.getSort()==null?0:n.getSort());
                })
                .collect(Collectors.toList());
        return collect;
    }

}